package com.radiant.data.entity;

import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import com.amazonaws.services.simpledb.AmazonSimpleDB;
import com.amazonaws.services.simpledb.model.Attribute;
import com.amazonaws.services.simpledb.model.DeleteAttributesRequest;
import com.amazonaws.services.simpledb.model.Item;
import com.amazonaws.services.simpledb.model.PutAttributesRequest;
import com.amazonaws.services.simpledb.model.ReplaceableAttribute;
import com.amazonaws.services.simpledb.model.ReplaceableItem;
import com.amazonaws.services.simpledb.model.SelectRequest;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.radiant.config.SimpleDBModule;
import com.radiant.data.annotation.Entity;
import com.radiant.data.annotation.Persist;
import com.radiant.data.annotation.PreProcessor;
import com.radiant.exceptions.RadiantRuntimeException;

/**
 * An abstract class that knows how to save an entity and retrieve an entity from Amazon's SimpleDB
 * @author karthikk
 *
 * @param <T>
 */
public abstract class SimpleDBDAO<T> extends SimpleDBCompoundType implements GenericDAO<T> {

	@Inject
	private AmazonSimpleDB sdb;
	@Inject
	protected PreProcessor preProcessor;

	private Class<T> persistentClass;
	private String domainName;

	public SimpleDBDAO(Class<T> persistentClass) {
		super();
		this.persistentClass = persistentClass;
	}
	
	public Class<T> getPersistentClass() {
		return persistentClass;
	}

	protected final String getDomainName() {
		if (domainName == null) {
			if (persistentClass.isAnnotationPresent(Entity.class)) {
				domainName = persistentClass.getAnnotation(Entity.class).name();
			} else {
				throw new RuntimeException("Entity annotation needed on " + persistentClass);
			}
		}
		return domainName;
	}

	
	public T newInstance() throws InstantiationException,
			IllegalAccessException {
		return persistentClass.newInstance();
	}

	public T findByPrimaryKey(String id, boolean consistentRead) {
		StringBuffer whereClause = new StringBuffer();
		whereClause.append(preProcessor.getItemName(persistentClass)).append(" = ");
		whereClause.append("'").append(id).append("'");
		List<T> items = query(whereClause.toString(), null, 0, consistentRead);
		if (!items.isEmpty()) {
			return items.get(0);
		} else {
			return null;
		}
	}

	public void delete(T entity) {
		String id =  null;
		try {
			id = (String)preProcessor.getItemReadMethod(persistentClass).invoke(entity, new Object[]{});
		} catch (Exception e) {
			throw new RadiantRuntimeException("Error reading item id of entity", e);
		}
		System.out.println("Deleting entity " + persistentClass + " with id " + id);
		sdb.deleteAttributes(new DeleteAttributesRequest(domainName, id));
	}

	@SuppressWarnings("unchecked")
	public T save(Object entity) {
		Field[] fields = persistentClass.getDeclaredFields();
		Field[] superFields = persistentClass.getSuperclass().getDeclaredFields();
		
		List<ReplaceableAttribute> attributes = new ArrayList<ReplaceableAttribute>();
		ReplaceableItem item = new ReplaceableItem();
		writeFields(item, attributes, fields, persistentClass, entity);
		writeFields(item, attributes, superFields, persistentClass, entity);
		sdb.putAttributes(new PutAttributesRequest(getDomainName(), item.getName(), attributes));
		return (T)entity;
	}
	
	private void writeFields(ReplaceableItem item, List<ReplaceableAttribute> attributes, 
			Field[] fields, Class<?> clazz, Object entity ) {
		for (Field f: fields) {
			if (f.isAnnotationPresent(Persist.class)) {
				try {
					Persist p = f.getAnnotation(Persist.class);
					String attributeName = p.name();
					PropertyDescriptor pd = new PropertyDescriptor(f.getName(), clazz);
					Object fieldValue = pd.getReadMethod().invoke(entity, new Object[]{});
					if (p.itemName()) {
						if (fieldValue == null) {
							fieldValue = UUID.randomUUID().toString();
							pd.getWriteMethod().invoke(entity, new Object[]{fieldValue});
						}
						item.setName(fieldValue.toString());
					}
					if (fieldValue != null) {
						if (p.multiValue()) {					
							for (Object listItem: (Collection<?>) fieldValue) {
								writeAttribute(attributes, p, attributeName, listItem);	
							}
						} else {
							writeAttribute(attributes, p, attributeName, fieldValue);	
						}
					}
				} catch (Exception e) {
					throw new RadiantRuntimeException("Error persisting field " + f.getName(), e);
				}
			}
		}
	}
	
	
	private void writeAttribute(List<ReplaceableAttribute> attributes, Persist p, String attributeName, Object value) 
			throws IllegalAccessException, InvocationTargetException, NoSuchMethodException, IntrospectionException {
		if (p.complex()) {
			Class<? extends GenericDAO<?>> daoClass = preProcessor.getDAOClass(value.getClass());
			GenericDAO<?> daoInstance = SimpleDBModule.getInjector().getInstance(daoClass);
			daoInstance.save(value);
		}
		String attributeValue = getBeanUtilsBean().getConvertUtils().convert(value);
		if (p.numeric()) {
			// do some padding
		}
		attributes.add(new ReplaceableAttribute(attributeName, attributeValue, true));						
	}

	protected List<T> query(String whereClause, String orderCol, int limit, boolean consistentRead) {
		List<T> listItems = new ArrayList<T>();
		List<Map<String,Object>> listAttribMaps = new ArrayList<Map<String, Object>>();
	    StringBuffer selectExpression = new StringBuffer("select * from `").append(getDomainName()).append("`");
	    if (whereClause != null) {
	    	selectExpression.append(" where ").append(whereClause);
	    }
	    if (orderCol != null) {
	    	selectExpression.append(" order by ").append(orderCol);
	    }
	    if (limit > 0) {
	    	selectExpression.append(" limit ").append(limit);
	    }
        System.out.println("Selecting: " + selectExpression + "\n");
        SelectRequest selectRequest = new SelectRequest(selectExpression.toString());
        selectRequest.setConsistentRead(consistentRead);
        int index = 0;
        for (Item item : sdb.select(selectRequest).getItems()) {
        	try {
	    		Map<String, Integer> multiValueCounterMap = new HashMap<String, Integer>();
	    		Map<String, Object> attribMap = new HashMap<String, Object>();
	    		listAttribMaps.add(attribMap);
	        	T newItem = persistentClass.newInstance();
	        	listItems.add(newItem);
	    		System.out.println("  Item");
	            System.out.println("    Name: " + item.getName());
	            for (Attribute attribute : item.getAttributes()) {
	                System.out.println("      Attribute");
	                System.out.println("        Name:  " + attribute.getName());
	                System.out.println("        Value: " + attribute.getValue());
	                String attribName = attribute.getName();
	                Object attribValue = attribute.getValue();
	                String fieldName = preProcessor.getFieldNameForAttribName(persistentClass, attribName);
	                if (preProcessor.isFieldMultiValue(persistentClass, attribName)) {
	                	List<?> listValue = 
	                			(List<?>)preProcessor.getFieldReadMethod(persistentClass, fieldName).invoke(newItem, new Object[]{});
	                	listValue.add(null);	
	                	index = (multiValueCounterMap.get(attribName) == null) ? 0 : multiValueCounterMap.get(attribName);
	                	attribValue = getBeanUtilsBean().getConvertUtils().convert(attribValue, 
	                			preProcessor.getMultiValueComponentType(persistentClass, attribName));
	                	fieldName = fieldName + "[" + index + "]";
	                    multiValueCounterMap.put(attribName, ++index);
	                }
	                attribMap.put(fieldName, attribValue);
	            }
        	} catch (Exception e) {
        		throw new RadiantRuntimeException("Error reading item " + item.getName() + " from simpleDB", e);
        	}
        }
        index  = 0;
        for (Map<String, Object> attribMap: listAttribMaps) {
        	try {
				getBeanUtilsBean().populate(listItems.get(index), attribMap);
			} catch (Exception e) {
				throw new RadiantRuntimeException("Error initalizing enity type " + this.persistentClass + " and index " + index, e);
			}
        	index++;
        }
        return listItems;
	}
	
}
